﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Runtime.CompilerServices;

using OmniKits.Extensions;

namespace OmniKits
{
    using Utilities;

    partial class Helpers
    {
        public sealed class SecureWebEncodingHelper { }
        public static SecureWebEncodingHelper SecureWebEncoding { get; }

        public static void SecureEncodeURIComponentForTextWriter(this SecureWebEncodingHelper _, IList<char> source, int start, int? count, TextWriter dest)
            => source.SecureEncodeURIComponentForTextWriter(start, count, dest);

        public static void SecureEncodeURIComponentForStream(this SecureWebEncodingHelper _, IList<char> source, int start, int? count, Stream dest)
            => source.SecureEncodeURIComponentForStream(start, count, dest);
    }
}

namespace OmniKits.Utilities
{
    using Core;

    public static class SecureWebEncodingUtility
    {
#pragma warning disable 1591

        public static void SecureEncodeURIComponentForTextWriter(this IList<char> source, int start, int? count, TextWriter dest)
        {
            if (start > source.Count || start < 0)
                throw new ArgumentException();

            // calc actual length & ending position
            int len, end;
            if (count.HasValue)
            {
                len = count.Value;
                end = start + len;

                if (end > source.Count || end < start)
                    throw new ArgumentException();
            }
            else
            {
                end = source.Count;
                len = source.Count - start;
            }

            var chars = new char[4]; // 4 is necessary to make UTF-8 bytes
            for (int i = start; i < end; i++)
            {
                var candidate = source[i];
                if (candidate < '\x80') // ASCII
                {
                    switch (candidate)
                    {
                        case '!':
                        //case '$': // ECMAScript compatibility
                        case '\'':
                        //case '+': // always escape due to classic form behavior
                        case '(':
                        case ')':
                        case '*':
                        //case ',': // ECMAScript compatibility
                        case '-':
                        case '.':
                        case '_':

                        // the ECMAScript version won't escape this one:
                        case '~':
                            break; // DO NOT escape chars shown above
                        default:

                            var escape = 
                                candidate < '0' || candidate > '9'
                                && candidate < 'A' || candidate > 'Z'
                                && candidate < 'a' || candidate > 'z';
                            if (escape)
                            {
                                dest.Write('%');
                                dest.Write(Defaults.HexChars[candidate >> 4]);
                                dest.Write(Defaults.HexChars[candidate & 15]);
                                continue;
                            }

                            break;
                    }

                    dest.Write(candidate);
                }
                else // Non-ASCII
                {
                    if (candidate.IsLowSurrogate())
                        throw new ArgumentException();

                    var c = (int)candidate;

                    // process a whole surrogate pair
                    if (candidate.IsHighSurrogate())
                    {
                        var second = source[++i];
#if PCL
                        c = 0x10000 + ((candidate & 0x3FF) << 10) + (second & 0x3FF);
#else
                        c = char.ConvertToUtf32(candidate, second);
#endif
                    }

                    // calc how many bytes is required for this code point
                    var hb = c.GetHighestBitPosition();
                    var cc = (hb + 4) / 5;
                    var p = cc;

                    // make following bytes
                    while (p-- > 1)
                    {
                        chars[p] = (char)((c & 0x3F) + 0x80);
                        c >>= 6;
                    }
                    // make leading byte
                    chars[0] = (char)(c + (((1 << cc) - 1) << (8 - cc)));

                    // write the bytes in hex form
                    for (p = 0; p < cc; p++)
                    {
                        candidate = chars[p];
                        chars[p] = '\0'; // cleaning up -- I think it's not necessary at all

                        dest.Write('%');
                        dest.Write(Defaults.HexChars[candidate >> 4]);
                        dest.Write(Defaults.HexChars[candidate & 15]);
                    }
                }
            }

            dest.Flush();
        }

        public static void SecureEncodeURIComponentForStream(this IList<char> source, int start, int? count, Stream dest)
        {
            using (var sw = new SecureStreamWriter(dest, false))
            {
                source.SecureEncodeURIComponentForTextWriter(start, count, sw);
            }
        }
    }
}
